#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.Xml;
#endregion // Using Statements

namespace ParticleSystem
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class ParticleSystemDemo : Microsoft.Xna.Framework.Game
    {
        #region Fields
        // Data related to rendering the scene
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        bool renderAsSprites;

        // particle system specific variables
        List<Force> forces;
        List<DrawablePlane> collisionPlanes;
        List<ParticleSystem> particleSystems;
        int particleSystemIndex;

        // camera variables
        Matrix worldMatrix, projectionMatrix, viewMatrix;
        Vector3 cameraPosition;
        float leftrightRot;
        float updownRot;
        const float rotationSpeed = 0.3f;
        const float moveSpeed = 30.0f;

        // variables dealing with rendering
        public static SpriteFont spriteFont;
        Model skyBoxModel;
        Texture2D[] skyBoxTextures;
        Effect skyBoxEffect;
        bool skyBoxEnabled;

        // input variables
        KeyboardState currentKeyboardState;
        KeyboardState lastKeyboardState;
        MouseState originalMouseState;
        MouseState currentMouseState;

        // File containing particle system demo specifications
        String xmlFileName;
        #endregion // Fields

        #region Constructors
        public ParticleSystemDemo(String xmlFileName)
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // default to rendering as sprites, not spheres
            renderAsSprites = true;

            // default to rendering sky box
            skyBoxEnabled = true;

            this.xmlFileName = xmlFileName;

            forces = new List<Force>();
            collisionPlanes = new List<DrawablePlane>();
            particleSystems = new List<ParticleSystem>();
        }
        #endregion // Constructors

        #region Loading, Unloading, and Initialization
        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            cameraPosition = new Vector3(0, 20.0f, 50.0f);
            leftrightRot = 0;
            updownRot = -MathHelper.Pi / 10.0f;

            float aspect = GraphicsDevice.Viewport.AspectRatio;

            worldMatrix = Matrix.Identity;
            UpdateViewMatrix();
            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(1, aspect, 1, 1000.0f);

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Load the sprite font 
            spriteFont = Content.Load<SpriteFont>("SpriteFont");

            skyBoxEffect = Content.Load<Effect>("effects");

            // Load the skybox model
            skyBoxModel = Utility.LoadModel(Content, skyBoxEffect, "skybox2", out skyBoxTextures);

            Mouse.SetPosition(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2);
            originalMouseState = Mouse.GetState();

            // no xml file name was provided
            if (xmlFileName.Equals(""))
            {
                particleSystems.Add(new ParticleSystem("test", new ParticleSettings(), GraphicsDevice, Content));
            }
            else
                LoadXml();

            // default to drawing the first system in the list
            particleSystemIndex = 0;
        }

        /// <summary>
        /// Load the particle system demo from xml file specifications.
        /// </summary>
        private void LoadXml()
        {
            if (!xmlFileName.EndsWith(".xml"))
                xmlFileName += ".xml";

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load("Content/" + xmlFileName);

            if (xmlDoc.ChildNodes.Count == 0)
                return;

            XmlElement rootElement = (XmlElement)xmlDoc.ChildNodes[0];

            foreach (XmlNode childNode in rootElement.ChildNodes)
            {
                if (!(childNode is XmlElement))
                    continue;

                XmlElement childElement = (XmlElement)childNode;

                // found global particle system properties
                if (childElement.Name.Equals("global"))
                {
                    foreach (XmlElement globalChildElement in childElement.ChildNodes)
                    {
                        // found a force
                        if (globalChildElement.Name.Equals("force"))
                        {
                            forces.Add(Force.ForceFromXml(globalChildElement));
                        }
                        // found a collision plane
                        else if (globalChildElement.Name.Equals("collisionPlane"))
                        {
                            collisionPlanes.Add(DrawablePlane.AxisAlignedPlaneFromXml(globalChildElement, GraphicsDevice));
                        }
                    }
                }
                // found a particle system
                else if (childElement.Name.Equals("particleSystem"))
                {
                    String particleSystemName = childElement.GetAttribute("name");
                    if (particleSystemName == null)
                        particleSystemName = "NoName";

                    particleSystems.Add(new ParticleSystem(particleSystemName, ParticleSettings.ParticleSettingsFromXml(childElement), GraphicsDevice, Content));
                }
            }
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }
        #endregion // Loading, Unloading, and Initialization

        #region Update and Draw
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;

            // handle user input
            HandleInput(elapsedSeconds);

            // update current particle system
            if(particleSystems.Count > 0)
                particleSystems.ElementAt(particleSystemIndex).Update(gameTime, forces, collisionPlanes);

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            // first draw the sky box if necessary
            if (skyBoxEnabled)
                DrawSkybox();

            // draw all planes in the demo
            foreach (DrawablePlane drawablePlane in collisionPlanes)
            {
                drawablePlane.Draw(worldMatrix, viewMatrix, projectionMatrix);
            }

            // draw the current particle system
            if (particleSystems.Count > 0)
            {
                // begin the sprite batch
                spriteBatch.Begin();
                // draw the particle system
                particleSystems.ElementAt(particleSystemIndex).Draw(renderAsSprites, spriteBatch, worldMatrix, viewMatrix, projectionMatrix);
                // we're done rendering this sprite batch
                spriteBatch.End();
            }

            // write out keyboard control text
            spriteBatch.Begin();
            spriteBatch.DrawString(spriteFont, "Keyboard Controls", new Vector2(10, 10), Color.White);
            spriteBatch.DrawString(spriteFont, "B: " + ((skyBoxEnabled) ? "Disable" : "Enable") + " sky box", new Vector2(10, 25), Color.White);
            spriteBatch.DrawString(spriteFont, "A-S-W-D/Arrow Keys: Move camera", new Vector2(10, 40), Color.White);
            spriteBatch.DrawString(spriteFont, "Q/Z: Move camera up or down", new Vector2(10, 55), Color.White);
            spriteBatch.DrawString(spriteFont, "R: Toggle particle rendering mode", new Vector2(10, 70), Color.White);
            spriteBatch.DrawString(spriteFont, "   (Currently " + (renderAsSprites ? "sprites" : "spheres") + ".)", new Vector2(10, 85), Color.White);
            spriteBatch.DrawString(spriteFont, "T: Toggle particle system", new Vector2(10, 100), Color.White);
            if (particleSystems.Count > 0)
                spriteBatch.DrawString(spriteFont, "   (Currently rendering " + particleSystems.ElementAt(particleSystemIndex).name + ")", new Vector2(10, 115), Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }

        private void DrawSkybox()
        {
            SamplerState ss = new SamplerState();
            ss.AddressU = TextureAddressMode.Clamp;
            ss.AddressV = TextureAddressMode.Clamp;
            GraphicsDevice.SamplerStates[0] = ss;

            DepthStencilState dss = new DepthStencilState();
            dss.DepthBufferEnable = false;
            GraphicsDevice.DepthStencilState = dss;

            Matrix[] skyboxTransforms = new Matrix[skyBoxModel.Bones.Count];
            skyBoxModel.CopyAbsoluteBoneTransformsTo(skyboxTransforms);
            int i = 0;
            foreach (ModelMesh mesh in skyBoxModel.Meshes)
            {
                foreach (Effect currentEffect in mesh.Effects)
                {
                    Matrix worldMatrix = skyboxTransforms[mesh.ParentBone.Index] * Matrix.CreateTranslation(cameraPosition);
                    currentEffect.CurrentTechnique = currentEffect.Techniques["Textured"];
                    currentEffect.Parameters["xWorld"].SetValue(worldMatrix);
                    currentEffect.Parameters["xView"].SetValue(viewMatrix);
                    currentEffect.Parameters["xProjection"].SetValue(projectionMatrix);
                    currentEffect.Parameters["xTexture"].SetValue(skyBoxTextures[i++]);
                }
                mesh.Draw();
            }

            dss = new DepthStencilState();
            dss.DepthBufferEnable = true;
            GraphicsDevice.DepthStencilState = dss;
        }
        #endregion // Update and Draw

        #region Camera Update Helpers
        /// <summary>
        /// Increment the camera's position.
        /// </summary>
        /// <param name="vectorToAdd">The amount by which the camera's position should be incremented.</param>
        private void AddToCameraPosition(Vector3 vectorToAdd)
        {
            Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
            Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation);
            cameraPosition += moveSpeed * rotatedVector;
            UpdateViewMatrix();
        }

        /// <summary>
        /// Update the view matrix based on camera position and direction.
        /// </summary>
        private void UpdateViewMatrix()
        {
            Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);

            Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
            Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);

            Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
            Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget;

            Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);

            viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);
        }
        #endregion // Camera Update Helpers

        #region Input
        /// <summary>
        /// Handle all user input.
        /// </summary>
        /// <param name="elapsedSeconds">The number of seconds elapsed since the last update.</param>
        private void HandleInput(float elapsedSeconds)
        {
            if (IsActive)
            {
                // handle mouse input first
                HandleMouseInput(elapsedSeconds);
                // now handle keyboard input
                HandleKeyboardInput(elapsedSeconds);
            }
        }

        /// <summary>
        /// Handle user input from the keyboard.
        /// </summary>
        /// <param name="elapsedSeconds">The number of seconds elapsed since the last update.</param>
        private void HandleKeyboardInput(float elapsedSeconds)
        {
            lastKeyboardState = currentKeyboardState;

            currentKeyboardState = Keyboard.GetState();

            Vector3 moveVector = new Vector3(0, 0, 0);

            // Check for exit.
            if (currentKeyboardState.IsKeyDown(Keys.Escape))
                Exit();

            // Check for switching render mode.
            if (currentKeyboardState.IsKeyDown(Keys.R) && !lastKeyboardState.IsKeyDown(Keys.R))
                renderAsSprites = !renderAsSprites;

            // Check for switching particle system.
            if (currentKeyboardState.IsKeyDown(Keys.T) && !lastKeyboardState.IsKeyDown(Keys.T))
                particleSystemIndex = (particleSystemIndex + 1) % particleSystems.Count;

            // Check for toggling sky box on/off.
            if (currentKeyboardState.IsKeyDown(Keys.B) && !lastKeyboardState.IsKeyDown(Keys.B))
                skyBoxEnabled = !skyBoxEnabled;

            // Check for forward camera movement.
            if (currentKeyboardState.IsKeyDown(Keys.Up) || currentKeyboardState.IsKeyDown(Keys.W))
                moveVector += new Vector3(0, 0, -1);

            // Check for backward camera movement.
            if (currentKeyboardState.IsKeyDown(Keys.Down) || currentKeyboardState.IsKeyDown(Keys.S))
                moveVector += new Vector3(0, 0, 1);

            // Check for right camera movement.
            if (currentKeyboardState.IsKeyDown(Keys.Right) || currentKeyboardState.IsKeyDown(Keys.D))
                moveVector += new Vector3(1, 0, 0);

            // Check for left camera movement.
            if (currentKeyboardState.IsKeyDown(Keys.Left) || currentKeyboardState.IsKeyDown(Keys.A))
                moveVector += new Vector3(-1, 0, 0);

            // Check for upward camera movement.
            if (currentKeyboardState.IsKeyDown(Keys.Q))
                moveVector += new Vector3(0, 1, 0);

            // Check for downward camera movement.
            if (currentKeyboardState.IsKeyDown(Keys.Z))
                moveVector += new Vector3(0, -1, 0);

            AddToCameraPosition(moveVector * elapsedSeconds);

        }

        /// <summary>
        /// Handle user input from the mouse.
        /// </summary>
        /// <param name="elapsedSeconds">The number of seconds elapsed since the last update.</param>
        private void HandleMouseInput(float elapsedSeconds)
        {
            currentMouseState = Mouse.GetState();
            if (currentMouseState != originalMouseState)
            {
                float xDifference = currentMouseState.X - originalMouseState.X;
                float yDifference = currentMouseState.Y - originalMouseState.Y;
                leftrightRot -= rotationSpeed * xDifference * elapsedSeconds;
                updownRot -= rotationSpeed * yDifference * elapsedSeconds;
                Mouse.SetPosition(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2);
                UpdateViewMatrix();
            }
        }
        #endregion // Input
    }
}
